FalconでRESTful Webサービスの実装 Part1
https://gyazo.com/f5d698fdcf41f507adad5c76ea259a0f
Falcon について
Falconは、PythonでRESTful Werbアプリケーションを作成するためのマイクロフレームワークで、WSGI(Web Server Gateway Interface) アプリケーションを開発できるものです。
Django とは違い自身ではORMやシリアライズ関連の機能や、開発用サーバを持っていないので他の拡張モジュールと連携してRESTful Webサービスを作成します。
インストール
既にCONDA環境になっているのであれば抜けておきましょう。
code: bash
$ conda deactivate
conda環境を作ります。このとき python と curl をインストールしておきます。
Webブラウザーを使用してすべてのタイプのHTTPリクエストを簡単に生成することはできないため、代わりに、curlを使用します。
code: bash
$ conda create -y -n falcon python=3.6 curl
$ conda activate falcon
Falcon と関連パッケージをインストールしておきましょう。
code: bash
$ pip install falcon gunicorn
Falconの動作確認
アプリケーションのディレクトリを作成します。
code: bash
$ mkdir -p $HOME/falcon/todo_apiv1
$ cd $HOME/falcon/todo_apiv1
動作確認のために app.py を作成します。
code: python
from falcon import API
import json
class Resource:
def on_get(self, req, resp):
greeting = {"greeting": "Hello World!" }
resp.body = json.dumps(greeting)
api = API()
api.add_route('/greeting', Resource())
falcon.API() は バージョン3.0 で falcon.App() に名前が変更される予定です。
add_route() は与えたURIにもとづいてリソースクラスで定義したメソッドにディスパッチします。HTTPプロトコルのGETメソッドが on_get() に対応します。
resp.media に設定されたオブジェクトがJSONに変換されて送信されます。
Falcon がサポートしているHTTPプロトコルのメソッドは、RFC 7231およびRFC 5789で指定されているメソッドで、GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、およびPATCHとなります。 Falcon は Flask のような開発用サーバを持っていないのでgunicorn で起動してみましょう。
code: bash
$ gunicorn --reload -b 127.0.0.1:8080 app:api
コードが変更されたときはリロードするようにオプション --reload を与えます。
デフォルトでは 127.0.0.1:8000 になっていますので、オプション-b 127.0.0.1:8080 を与えて起動しています。
別のターミナルで次のように実行してみましょう。
code: bash
{
"greeting": "Hello World!"
}
JSONフォーマットで返された結果を json.tool を使って整形して表示させています。
期待どおりに "Hello World!" 出力されれば Falcon の動作は問題ありません。
ミドルウェア
REST API では 、軽量なJSON形式でデータを送受信することが一般的です。受け取ったリクエストデータの解析や検証を行う必要がありますが、Falcon 1.3 以降 req.media() と resp.media() が提供されてこの処理について開発者が気にする必要がなくなりました。
code: python
class Resource:
def on_get(self, req, resp):
resp.media = {"greeting": "Hello World!" }
Falcon 1.3 より前のバージョンではどうしていたかを含めてミドルウェアについて説明しておきます。
Falcon にはアプリケーション全体に対して処理が適用されるミドルウェアという仕組みがありま。ミドルウェアは次のメソッドを持つクラスを定義するようにします。
process_request(): リクエストを受信後に呼び出される
process_resource() :リソース(Resource)の到達前に呼び出される
process_response(): レスポンスを返す前に呼び出される
process_resource() は、URIに対するリソースが存在しないときは実行されませんが、
process_request()、process_response() は、リクエストされると必ず実行されます。
ミドルウェアは次の’ように falcon.API()の middlewareキーワードにリストとしてクラスインスタンスを登録します。
code: python
class RequireJSON:
def process_request(self):
pass
class JSONTranslate:
def process_request(self):
pass
def process_response(self):
pass
api = falcon.API(middleware=[
RequireJSON(),
JSONTranslator(),
])
JSONハンドラの変更
例えば python-rapidjson は C/C++ 実装された軽量で高速なJSONシリアライズ/ディシリアライズライブラリですが、これをFalcon がデフォルトで持っているJSONハンドラと変更したいときは次のようにします。
code: python
import falcon
from falcon import media
import rapidjson
json_handler = media.JSONHandler(
dumps=rapidjson.dumps,
loads=rapidjson.loads,
)
extra_handlers = {
'application/json': json_handler,
}
api = falcon.API()
api.req_options.media_handlers.update(extra_handlers)
api.resp_options.media_handlers.update(extra_handlers)
クエリパラメタの取得
クエリパラメタとして受け取る場合は次のようreq.get_param() を使います。
code: python
class Resource:
def on_get(self, req, resp):
name = req.get_param('name')
resp.media = {"greeting": f"Hello {name}!" }
code: bash
{
"greeting": "Hello Freddie!"
}
get_param(name, required=False, store=None, default=None)
name: パラメタのキーワード
required:True/False、パラメタが必須がどうかのブール値
default:パラメタのデフォルト値
get_param() は型指定を行うこともできます。
get_param_as_int(): パラメタ名で指定した文字列を整数に変換して返す
get_param_as_float():パラメタ名で指定した文字列を浮動小数点に変換して返す
get_param_as_bool():パラメタ名で指定した文字列をブール値に変換して返す
get_param_as_list():パラメタ名で指定した文字列をリストとして返す
get_param_as_date():パラメタ名で指定した文字列をDate型に変換して返す
get_param_as_datetime():パラメタ名で指定した文字列をDateTime型に変換して返す
get_param_as_json():パラメタ名で指定した文字列をJSON型で返す
get_param_as_uuid():パラメタ名で指定した文字列をUUIDとして返す
フォームからパラメタを取得
POSTされたフォームからパラメタを取得するときは次のようにします。
code: python
api = falcon.API()
api.req_options.auto_parse_form_urlencoded=True
auto_parse_form_urlencoded=True のとき(デフォルトはFalse)、
リクエストのコンテンツタイプがapplication/x-www-form-urlencoded であれば、リクエストのクエリ文字列パラメーターにマージします。
これにより、リソースクラスのメソッドでget_param() を使ってパラメタを取得することができます。
URIとしてパラメタを取得
URIのパスにパラメタを含める場合は次のように定義します。
code: python
from falcon import API
class Resource:
def on_get(self, req, resp, name):
resp.media = {"greeting": f"Hello {name}!" }
api = API()
api.add_route('/greeting/{name}', Resource())
code: bash
{
"greeting": "Hello Freddie!"
}
ただし、クエリパラメタとURIパスを混在したアプリケーションは統一されたインタフェースとならず避けるべきです。REST APIの設計ルールからみても問題となります。
HTTPメソッドのマッピング
WebサービスTODOの仕様を少し変更して、PUTメソッドではなくPATCHメソッドを使うようにしています。
table: APIとHTTPメソッド
HTTPメソッド URI アクション
タスクリソースは次の情報を持つものとします。
id:タスクを示す一意の識別子。Integer型。
title:タスクのタイトル。タスクについての短い説明。 String型。
description:タスクの詳細。タスクについての詳細な説明。 Text型。
done:タスクの完了状態。 Boolean型。
タスクリソースの定義
タスクリソース今回はREST APIに集中するために単純に辞書型のリストとして定義します。
code: tasks.py
tasks = [
{
'id': 1,
'title': 'Buy Beer',
'description': 'IPA 6 bottles',
'done': False
},
{
'id': 2,
'title': 'Buy groceries',
'description': 'Beef, Tofu, Sting Onion',
'done': False
}
]
GETメソッドでタスク一覧を取得
app.py を定義しましょう。
code: python
from falcon import API
from tasks import tasks
class TaskListResource:
def on_get(self, req, resp):
resp.media = tasks
api = API()
api.add_route('/todo/api/v1.0/tasks', TaskListResource())
このままでは、判読しづらいので次のようにして整形してみましょう。
code: bash
[
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
},
{
"id": 2,
"title": "Buy groceries",
"description": "Beef, Tofu, Sting Onion",
"done": false
}
]
GETメソッドで指定したタスクを取得
指定したタスクを取得するようにしましょう。
code: python
from falcon import API
from tasks import tasks
class TaskListAPI:
def on_get(self, req, resp):
resp.media = {'tasks': tasks }
class TaskAPI:
def on_get(self, req, resp, id):
task_id = int(id) - 1
resp.media = {'tasks': taskstask_id } api = API()
api.add_route('/todo/api/v1.0/tasks', TaskListAPI())
api.add_route('/todo/api/v1.0/tasks/{id}', TaskAPI())
add_route() に与える URI で {id} と記述するとその部分が変数 id としてon_get()メソッドで受け取れるようになります。
次のようにして実行します。
code: bash
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
}
このとき、存在しないIDを与えると問題となります。
アプリケーションで IndexError の例外が発生してしまいます。
code: python
...
responder(req, resp, **params)
File "/Users/goichiiisaka/Downloads/Python.Osaka/RESTfulAPIClass/falcon/todo_apiv1/app.py", line 45, in on_get
resp.media = {'tasks': taskstask_id)} IndexError: list index out of range
応答についてもJSONが返されていませんし、内容もよくわかりません。
code: bash
Expecting value: line 1 column 1 (char 0)
そこで次のようにしましょう。
code: python
class TaskResource:
def on_get(self, req, resp, id):
task_id = int(id) - 1
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
raise falcon.HTTPError(falcon.HTTP_404)
else:
POSTメソッドでタスクを登録
次に、POSTをメソッドでタスクを登録できるようにしましょう。
さて、
code: python
import falcon
from tasks import tasks
class TaskListResource:
def on_get(self, req, resp):
resp.media = {'tasks': tasks }
def on_post(self, req, resp):
task = req.media
task'id' = len(tasks) + 1 tasks.append(task)
resp.media = {'result': 'OK'}
class TaskResource:
def on_get(self, req, resp, id):
task_id = int(id) - 1
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
raise falcon.HTTPError(falcon.HTTP_404)
else:
def error_handle_404(req, resp):
resp.status = falcon.HTTP_404
resp.media = {'result': 'Not found' }
api = falcon.API()
api.add_sink(error_handle_404)
api.add_route('/todo/api/v1.0/tasks', TaskListResource())
api.add_route('/todo/api/v1.0/tasks/{id}', TaskResource())
add_sink() について
api.add_sink() で無効なURIに対してのエラーハンドラを設定しています。
リクエストに一致するURIがadd_route() に登録されていないときは、リクエストされたHTTPメソッドに関係なく、関連付けられた関数に制御を渡します。
アプリケーションで静的リソースやレスポンダを作成することが好ましくないか現実的でないときに、リクエストを1つ以上のバックエンドサービスに転送するプロキシを作成することができます。
code: bash
{
"result": "OK"
}
code: bash
{
"tasks": [
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
},
{
"id": 2,
"title": "Buy groceries",
"description": "Beef, Tofu, Sting Onion",
"done": false
},
{
"title": "Gymnastics",
"description": "Goto Anytime Fitness",
"id": 3,
"done": false
}
]
}
PATCHメソッドでタスクを更新
次にHTTPプロトコルPATCHメソッドで指定したタスクを更新できるようにしましょう。
code: python
class TaskResource:
def on_get(self, req, resp, id):
task_id = int(id) - 1
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
raise falcon.HTTPError(falcon.HTTP_404)
else:
def on_patch(self, req, resp, id):
req_task = req.media
task_id = int(id) - 1
task = [task for task in tasks if task'id' == task_id] if len(req_task) == 0:
raise falcon.HTTPError(falcon.HTTP_404)
if key in req_task.keys():
resp.media = {'tasks': taskstask_id} はじめにタスク一覧を表示して現在の登録内容を確認しておきましょう。
code: bash
{
"tasks": [
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
},
{
"id": 2,
"title": "Buy groceries",
"description": "Beef, Tofu, Sting Onion",
"done": false
}
]
}
IDが2のタスクを変更してみます。
code: bash
{
"tasks": {
"id": 2,
"title": "Drink Beer",
"description": "I Love IPA",
"done": false
}
}
もう一度タスク一覧を表示してみます。
code: bash
{
"tasks": [
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
},
{
"id": 2,
"title": "Drink Beer",
"description": "I Love IPA",
"done": false
}
]
}
DELETEメソッドでタスクを削除
HTTPプロトコルDELETEメソッドで削除することも簡単です。
code: python
class TaskResource:
# ...
def on_delete(self, req, resp, id):
task_id = int(id) - 1
task = [task for task in tasks if task'id' == task_id] if len(req_task) == 0:
raise falcon.HTTPError(falcon.HTTP_404)
else:
resp.media = {'result': 'OK'}
追加してから削除してみましょう。
code: bash
{
"result": "OK"
}
code: bash
{
"tasks": [
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
},
{
"id": 2,
"title": "Drink Beer",
"description": "I Love IPA",
"done": false
},
{
"title": "Gymnastics",
"description": "Goto Anytime Fitness",
"id": 3,
"done": false
}
]
}
code: bash
{
"result": "OK"
}
code: basg
{
"tasks": [
{
"id": 1,
"title": "Buy Beer",
"description": "IPA 6 bottles",
"done": false
},
{
"id": 2,
"title": "Drink Beer",
"description": "I Love IPA",
"done": false
}
]
}
参考: